package com.atguigu.gmall.seckill.service.impl;

import com.atguigu.gmall.common.cache.GmallCache;
import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.MD5;
import com.atguigu.gmall.model.activity.SeckillGoods;
import com.atguigu.gmall.model.order.OrderInfo;
import com.atguigu.gmall.mq.MqConst;
import com.atguigu.gmall.seckill.component.SeckillLocalCache;
import com.atguigu.gmall.seckill.mapper.SeckillGoodsMapper;
import com.atguigu.gmall.seckill.service.SeckilService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author lsj
 * @create 2021-09-02 11:22
 */
@Service
public class SeckillServiceImpl extends ServiceImpl<SeckillGoodsMapper, SeckillGoods> implements SeckilService {

    @Autowired
    SeckillGoodsMapper seckillGoodsMapper;

    @Autowired
    SeckilService seckilService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    SeckillLocalCache localCache;


    @Autowired
    RedissonClient redissonClient;

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Autowired
    ThreadPoolExecutor executor;

    /**
     * 给redis中缓存一份
     * seckill:goods:2021-5-11  [{},{}]
     *
     *   formatdate：当前日期
     *   formattime：当前时间
     * 切面？
     *      当前Service的代理对象
     * @return
     */
    @GmallCache(enableBloom = false,cacheKeyExpr = RedisConst.SECKILL_GOODS +"#{#formatdate}",ttl = 60*60*24*1000)
    @Override
    public List<SeckillGoods> getCurrentSeckillGoods() {
        List<SeckillGoods> goods = seckillGoodsMapper.getCurrentSeckillGoods();
        //也可以把所有商品做成一个hash
        return goods;
    }

    @Override
    public String generateSeckillCode(Long skuId, String userId) {
        //TODO 商品秒杀时间到了，才生成秒杀码
        SeckillGoods goods = localCache.getSeckillGoodsBySkuId(skuId);
        if(goods.getStartTime().getTime() <= System.currentTimeMillis()){
            //秒杀时间判断，秒杀已经开始了
            String encrypt = MD5.encrypt(skuId.toString() + "_" + userId);
            return encrypt;
        }

        return "error";
    }

    @Override
    public Boolean checkSeckillCode(Long skuId, String userId, String code) {
        String encrypt = MD5.encrypt(skuId.toString() + "_" + userId);
        if(encrypt.equals(code)){
            return true;
        }
        return false;
    }

    @Override
    public List<SeckillGoods> getCurrentSeckillGoodsFromCache() {

        List<SeckillGoods> goods = localCache.todaySeckillGoods();
        return goods;
    }

    @Override
    public SeckillGoods getSeckillGoodsById(Long skuId) {
        return localCache.getSeckillGoodsBySkuId(skuId);
    }

    @Override
    public boolean checkRepeat(Long skuId, String userId, String skuIdStr) {
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        SeckillGoods goods = localCache.getSeckillGoodsBySkuId(skuId);
        long l = goods.getEndTime().getTime() - System.currentTimeMillis();
        //活动期间，这个商品只能秒一次，这是占位   （setnx，即能存进去）
        Boolean absent = operations.setIfAbsent(RedisConst.SECKILL_USER + skuIdStr, "", l, TimeUnit.MILLISECONDS);
        return absent;
    }

    @Override
    public boolean seckillStockLocked(Long skuId, String userId, String skuIdStr) {
        RSemaphore semaphore = redissonClient.getSemaphore(RedisConst.SECKILL_STOCK_PREFIX + skuId.toString());

        //不阻塞，快速尝试获取信号量
        boolean b = semaphore.tryAcquire();

        //顺便扣一下cache中的商品缓存,本地状态位的延迟更新
        SeckillGoods goods = localCache.getSeckillGoodsBySkuId(skuId);
        if((goods.getNum() - goods.getStockCount())>0){
            //重新修改本地的状态位
            goods.setStockCount(goods.getStockCount() + 1);
        }

        if(!b){
            //秒杀完了。查看可用信号还有多少，仅是查看不是扣，tryAcquire才是扣
            int i = semaphore.availablePermits();  //假设10w进来
            if(i==0){ //
                //给兄弟通知，当前商品秒杀结果。为啥不发消息队列。若并发，将重复通知很多遍。浪费性能的操作，线程池有拒绝策略所以并发进来的并不会每个都执行
                executor.submit(()->{
                    //提交10w，只留最后几个最新通知。50，通知很多商品，有些商品没有及时更新过来
                    //发出的消息，交给线程池。线程池队列4很小
                    redisTemplate.convertAndSend(RedisConst.SECKILL_GOODS_OUT,skuId.toString());
                });

            }
        }

        return b;
    }

    @Override
    public ResultCodeEnum checkOrderStatus(Long skuId, String userId) throws JsonProcessingException {
        //1、先看秒杀单是否存在redis中，并且，已经确认过秒杀单信息（填充了，收货地址信息）
        String orderJson = redisTemplate.opsForValue().get(RedisConst.SECKILL_USER + userId);
        if(StringUtils.isEmpty(orderJson)){
            //说明redis没有生成订单。  可能状态是：正在排队，队排过了，但是运气不好，没有抢到。查看防重下单码有没有占过位
            Boolean code = redisTemplate.hasKey(RedisConst.SECKILL_USER + MD5.encrypt(skuId.toString() + "_" + userId));
            if(code){
                //redis中占过位  //排了队。   内存状态位校验通过了。  是否在排队中，要真正确认。
                //检查是否是由于没库存导致的。
                SeckillGoods goods = localCache.getSeckillGoodsBySkuId(skuId);
                if((goods.getNum()-goods.getStockCount())>0){
                    //有库存
                    return ResultCodeEnum.SECKILL_RUN;
                }
                return ResultCodeEnum.SECKILL_FINISH;
            }else {
                //没有排队。 内存状态位校验都没有通过
                return ResultCodeEnum.SECKILL_FINISH;
            }
        }else {
            //说明下过单
            OrderInfo orderInfo = new ObjectMapper().readValue(orderJson, OrderInfo.class);
            if(StringUtils.isEmpty(orderInfo.getConsignee())){
                //我们还没有确认订单。
                return ResultCodeEnum.SECKILL_SUCCESS;
            }else {
                //我们已经确定了订单
                return ResultCodeEnum.SECKILL_ORDER_SUCCESS;
            }
        }
    }

    @Override
    public OrderInfo getUserSeckillOrderInfo(String userId) throws JsonProcessingException {
        String json = redisTemplate.opsForValue().get(RedisConst.SECKILL_USER + userId);
        return new ObjectMapper().readValue(json,OrderInfo.class);
    }

    @Override
    public void clearBeforeDatas() throws JsonProcessingException {
        // 1、把seckill:旗下的清除。
        // 拿到昨天的上架的所有商品：seckill:goods:2021-08-27
        String s = LocalDate.now().minusDays(1).toString();
        String json = redisTemplate.opsForValue().get(RedisConst.SECKILL_GOODS + s);
        if (json == null) {
            // json不应该为空，如果为空表示有问题，应该删去
            log.error("redis中的上架商品为空");
            redisTemplate.delete(RedisConst.SECKILL_GOODS + s);
            log.error("redis已删除空数据");
        } else {
            List<SeckillGoods> seckillGoods = new ObjectMapper().readValue(json, new TypeReference<List<SeckillGoods>>() {
            });

            //1）、清理昨天商品
            redisTemplate.delete(RedisConst.SECKILL_GOODS + s);
            //2）、更新库存占位
            for (SeckillGoods good : seckillGoods) {
                Long skuId = good.getSkuId();
                //看当前商品信号量
                RSemaphore semaphore = redissonClient.getSemaphore(RedisConst.SECKILL_STOCK_PREFIX + skuId.toString());
                int i = semaphore.availablePermits(); //2,6,0

                good.setStockCount(good.getNum() - i);//代表卖了多少件
                seckillGoodsMapper.updateById(good);//按照id一更新
            }
        }
    }


    /**
     * 提前对缓存进行预热
     * 定时上架秒杀商品
     * 秒 分 时  日 月 周
     *
     * 面试问题：
     * A:
     *  @Transaction(timeout=3)
     *  a{
     *
     *  }
     *
     *  @Transaction(timeout=5，pro)
     *  b{
     *
     *  }
     *
     *  请问为什么？【自己调用自己的方法，方法的切面功能失效】
     *  1、A.a();  A.b();都是成功
     *  2、A.a(){
     *      b();//b的事务的所有设置失效？？？？，b的代码复制到a()
     *  }
     *  Spring中见过使用注解完成的功能，都是使用代理对象进行控制。
     *  只要不是 [代理对象.方法()]  就是功能失效。
     *  解决：
     *  A{
     *      @Autowired
     *      A a;
     *
     *      a(){
     *          a.b();//生效
     *          b();//代理的自定义功能失效
     *      }
     *
     *      @Transaction
     *      b(){
     *
     *      }
     *  }
     *
     *
     */
//    @Scheduled(cron = "0/3 * * * * ?")
    @Scheduled(cron = "0 0 3 * * ?")
    public void scheduledUpSeckillGoods(){
        System.out.println("上架当天需要秒杀的商品.....已经给消息队列发送消息"); //为什么

        String message = new SimpleDateFormat("yyyy-MM-dd").format(new Date());

        rabbitTemplate.convertAndSend(MqConst.SECKILL_EXCHANGE,MqConst.SECKILL_UPGOOD_RK,message);

        //3、准备分布式信号量。



    }

}
